package com.jroky.base.mybatis.interceptor;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.commons.lang3.StringUtils;

import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class LikeSqlInnerInterceptor implements InnerInterceptor {

    public static final String DOT = ".";
    public static final String PLACEHOLDER_REGEX = "\\?";
    public static final String DOT_REGEX = "\\.";
    public static final char LIKE_WILDCARD_CHARACTER = '%';
    public static final String PLACEHOLDER = " ?";
    public static final String WRAPPER_PARAMETER_PROPERTY = "ew.paramNameValuePairs.";
    public static final String LIKE_SQL = " like ";
    public static final String IGNORE = "EscapeLikeSqlIgnore";
    public static final String SQL_SPECIAL_CHARACTER = "\\_%*@|&()[]\"'";
    public static final String SQL_TRANS_CHARACTER = "\\";

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        if (parameter instanceof Map){
            Map<?,?> parameterMap = (Map<?,?>) parameter;
            if (parameterMap.containsKey(IGNORE)){
                return;
            }
        }

        if (!needEscape(boundSql.getSql())){
            return;
        }

        escapeSql(boundSql);
    }
    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        if (parameter instanceof Map){
            Map<?,?> parameterMap = (Map<?,?>) parameter;
            if (parameterMap.containsKey(IGNORE)){
                return;
            }
        }

        BoundSql boundSql = ms.getBoundSql(parameter);
        if (!needEscape(boundSql.getSql())){
            return;
        }

        escapeSql(boundSql);
    }

    private void escapeSql(BoundSql boundSql) {
        String[] split = boundSql.getSql().split(PLACEHOLDER_REGEX);
        Object parameter = boundSql.getParameterObject();
        Set<String> processedProperty = new HashSet<>();

        for (int i = 0; i < split.length; i++) {
            if (StrUtil.lastIndexOfIgnoreCase(split[i],LIKE_SQL)>-1){
                if (parameter instanceof Map){
                    String property = boundSql.getParameterMappings().get(i).getProperty();
                    if (processedProperty.contains(property)){
                        continue;
                    }
                    Map<Object,Object> parameterMap = (Map<Object, Object>) parameter;
                    if (containWrapper(property)){
                        handlerWrapperEscape(property,parameterMap);
                    }else {
                        handlerOriginalSqlEscape(property,parameterMap);
                    }
                    processedProperty.add(property);
                } else if (parameter instanceof String) {
                    BeanUtil.setFieldValue(boundSql.getParameterObject(),"value",addSalashes(((String) parameter)).toCharArray());
                } else if (parameter != null) {
                    MetaObject metaObject = SystemMetaObject.forObject(parameter);
                    String[] setterNames = metaObject.getSetterNames();
                    for (String setterName : setterNames) {
                        Class<?> setterType = metaObject.getSetterType(setterName);
                        if (String.class.equals(setterType)){
                            String value = (String) metaObject.getValue(setterName);
                            metaObject.setValue(setterName,addSalashes((value)));
                        }
                    }
                }
            }

        }
    }

    private boolean containWrapper(String property) {
        return StrUtil.contains(property,WRAPPER_PARAMETER_PROPERTY);
    }

    private String addSalashes(CharSequence parameter) {
        if (StrUtil.isEmpty(parameter)){
            return StrUtil.str(parameter);
        }
        char[] chars = SQL_SPECIAL_CHARACTER.toCharArray();

        for (char c : chars) {
            parameter = StrUtil.replace(parameter, StringUtils.join(SQL_TRANS_CHARACTER,c),String.valueOf(c));
        }

        for (char c : chars) {
            parameter = StrUtil.replace(parameter,String.valueOf(c),StringUtils.join(SQL_TRANS_CHARACTER,c));
        }
        return parameter.toString();
    }

    private String addSalashes(CharSequence parameter,char trimFix) {
        if (parameter.charAt(0) == trimFix){
            parameter = parameter.subSequence(1,parameter.length());
        }
        if (parameter.charAt(parameter.length()-1) == trimFix){
            parameter = parameter.subSequence(0,parameter.length()-1);
        }

        return addSalashes(parameter);
    }


    private void handlerWrapperEscape(String property, Map<Object, Object> parameterMap) {
        String[] keys = property.split(DOT_REGEX);
        Object ew = parameterMap.get(keys[0]);
        if (ew instanceof AbstractWrapper){
            Map<String,Object> paramNameValuePairs = ((AbstractWrapper<?,?,?>) ew).getParamNameValuePairs();
            Object paramValue = paramNameValuePairs.get(keys[2]);
            if (paramValue instanceof String){
                paramNameValuePairs.put(keys[2],String.format("%%%s%%",addSalashes((String) paramValue,LIKE_WILDCARD_CHARACTER)));
            }
        }
    }

    private void handlerOriginalSqlEscape(String property, Map<Object, Object> parameterMap) {
        if (cascadeParameter(property)){
            String[] keys = property.split(DOT_REGEX,2);
            Object parameterBean = parameterMap.get(keys[0]);
            Object parameterValue = BeanUtil.getProperty(parameterBean,keys[1]);

            if (parameterValue instanceof String){
                BeanUtil.setProperty(parameterBean,keys[1],addSalashes((CharSequence) parameterValue));
            }
        }else {
            parameterMap.computeIfPresent(property,(key, value)->{
                if (value instanceof String){
                    return addSalashes((CharSequence) value);
                }
                return value;
            });
        }
    }

    private boolean cascadeParameter(String property) {
        return StrUtil.contains(property,DOT);
    }

    private boolean needEscape(String sql) {
        return containLike(sql) && containPlaceholder(sql);
    }

    private boolean containPlaceholder(String sql) {
        return StrUtil.containsAnyIgnoreCase(sql,LIKE_SQL);
    }

    private boolean containLike(String sql) {
        return StrUtil.containsAnyIgnoreCase(sql,LIKE_SQL);
    }
}
